1   /*
2    * Copyright (C) 2006 The Guava Authors
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  
17  package com.google.common.util.concurrent;
18  
19  import static com.google.common.base.Preconditions.checkArgument;
20  import static com.google.common.base.Preconditions.checkNotNull;
21  
22  import com.google.common.annotations.Beta;
23  import com.google.common.collect.ObjectArrays;
24  import com.google.common.collect.Sets;
25  
26  import java.lang.reflect.InvocationHandler;
27  import java.lang.reflect.InvocationTargetException;
28  import java.lang.reflect.Method;
29  import java.lang.reflect.Proxy;
30  import java.util.Set;
31  import java.util.concurrent.Callable;
32  import java.util.concurrent.ExecutionException;
33  import java.util.concurrent.ExecutorService;
34  import java.util.concurrent.Executors;
35  import java.util.concurrent.Future;
36  import java.util.concurrent.TimeUnit;
37  import java.util.concurrent.TimeoutException;
38  
39  /**
40   * A TimeLimiter that runs method calls in the background using an
41   * {@link ExecutorService}.  If the time limit expires for a given method call,
42   * the thread running the call will be interrupted.
43   *
44   * @author Kevin Bourrillion
45   * @since 1.0
46   */
47  @Beta
48  public final class SimpleTimeLimiter implements TimeLimiter {
49  
50    private final ExecutorService executor;
51  
52    /**
53     * Constructs a TimeLimiter instance using the given executor service to
54     * execute proxied method calls.
55     * <p>
56     * <b>Warning:</b> using a bounded executor
57     * may be counterproductive!  If the thread pool fills up, any time callers
58     * spend waiting for a thread may count toward their time limit, and in
59     * this case the call may even time out before the target method is ever
60     * invoked.
61     *
62     * @param executor the ExecutorService that will execute the method calls on
63     *     the target objects; for example, a {@link
64     *     Executors#newCachedThreadPool()}.
65     */
66    public SimpleTimeLimiter(ExecutorService executor) {
67      this.executor = checkNotNull(executor);
68    }
69  
70    /**
71     * Constructs a TimeLimiter instance using a {@link
72     * Executors#newCachedThreadPool()} to execute proxied method calls.
73     *
74     * <p><b>Warning:</b> using a bounded executor may be counterproductive! If
75     * the thread pool fills up, any time callers spend waiting for a thread may
76     * count toward their time limit, and in this case the call may even time out
77     * before the target method is ever invoked.
78     */
79    public SimpleTimeLimiter() {
80      this(Executors.newCachedThreadPool());
81    }
82  
83    @Override
84    public <T> T newProxy(final T target, Class<T> interfaceType,
85        final long timeoutDuration, final TimeUnit timeoutUnit) {
86      checkNotNull(target);
87      checkNotNull(interfaceType);
88      checkNotNull(timeoutUnit);
89      checkArgument(timeoutDuration > 0, "bad timeout: %s", timeoutDuration);
90      checkArgument(interfaceType.isInterface(),
91          "interfaceType must be an interface type");
92  
93      final Set<Method> interruptibleMethods
94          = findInterruptibleMethods(interfaceType);
95  
96      InvocationHandler handler = new InvocationHandler() {
97        @Override
98        public Object invoke(Object obj, final Method method, final Object[] args)
99            throws Throwable {
100         Callable<Object> callable = new Callable<Object>() {
101           @Override
102           public Object call() throws Exception {
103             try {
104               return method.invoke(target, args);
105             } catch (InvocationTargetException e) {
106               throwCause(e, false);
107               throw new AssertionError("can't get here");
108             }
109           }
110         };
111         return callWithTimeout(callable, timeoutDuration, timeoutUnit,
112             interruptibleMethods.contains(method));
113       }
114     };
115     return newProxy(interfaceType, handler);
116   }
117 
118   // TODO: should this actually throw only ExecutionException?
119   @Override
120   public <T> T callWithTimeout(Callable<T> callable, long timeoutDuration,
121       TimeUnit timeoutUnit, boolean amInterruptible) throws Exception {
122     checkNotNull(callable);
123     checkNotNull(timeoutUnit);
124     checkArgument(timeoutDuration > 0, "timeout must be positive: %s",
125         timeoutDuration);
126     Future<T> future = executor.submit(callable);
127     try {
128       if (amInterruptible) {
129         try {
130           return future.get(timeoutDuration, timeoutUnit);
131         } catch (InterruptedException e) {
132           future.cancel(true);
133           throw e;
134         }
135       } else {
136         return Uninterruptibles.getUninterruptibly(future, 
137             timeoutDuration, timeoutUnit);
138       }
139     } catch (ExecutionException e) {
140       throw throwCause(e, true);
141     } catch (TimeoutException e) {
142       future.cancel(true);
143       throw new UncheckedTimeoutException(e);
144     }
145   }
146 
147   private static Exception throwCause(Exception e, boolean combineStackTraces)
148       throws Exception {
149     Throwable cause = e.getCause();
150     if (cause == null) {
151       throw e;
152     }
153     if (combineStackTraces) {
154       StackTraceElement[] combined = ObjectArrays.concat(cause.getStackTrace(),
155           e.getStackTrace(), StackTraceElement.class);
156       cause.setStackTrace(combined);
157     }
158     if (cause instanceof Exception) {
159       throw (Exception) cause;
160     }
161     if (cause instanceof Error) {
162       throw (Error) cause;
163     }
164     // The cause is a weird kind of Throwable, so throw the outer exception.
165     throw e;
166   }
167 
168   private static Set<Method> findInterruptibleMethods(Class<?> interfaceType) {
169     Set<Method> set = Sets.newHashSet();
170     for (Method m : interfaceType.getMethods()) {
171       if (declaresInterruptedEx(m)) {
172         set.add(m);
173       }
174     }
175     return set;
176   }
177 
178   private static boolean declaresInterruptedEx(Method method) {
179     for (Class<?> exType : method.getExceptionTypes()) {
180       // debate: == or isAssignableFrom?
181       if (exType == InterruptedException.class) {
182         return true;
183       }
184     }
185     return false;
186   }
187 
188   // TODO: replace with version in common.reflect if and when it's open-sourced
189   private static <T> T newProxy(
190       Class<T> interfaceType, InvocationHandler handler) {
191     Object object = Proxy.newProxyInstance(interfaceType.getClassLoader(),
192         new Class<?>[] { interfaceType }, handler);
193     return interfaceType.cast(object);
194   }
195 }